Skip to content

refactor(openai-bridge): split runtime retry layers#212

Merged
ymkiux merged 5 commits into
mainfrom
feat/codex-bridge-retry-config
Jul 11, 2026
Merged

refactor(openai-bridge): split runtime retry layers#212
ymkiux merged 5 commits into
mainfrom
feat/codex-bridge-retry-config

Conversation

@awsl233777

@awsl233777 awsl233777 commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • split Codex OpenAI bridge conversion/HTTP/SSE runtime into cli/openai-bridge-runtime.js
  • split transient retry policy into cli/openai-bridge-retry.js while keeping provider maxRetries wiring
  • add max=10 UI boundary for built-in transform retry count and refresh precompiled Web UI render

Validation

  • npm run lint
  • npm run test:unit
  • npm run test:e2e
  • git diff --check
  • real Web UI screenshot: /tmp/codexmate-e2e-screens/07-add-provider-transform-retry.png
  • real Web UI screenshot: /tmp/codexmate-e2e-screens/08-retry-min-validation.png

Summary by CodeRabbit

  • New Features

    • Added a “maximum retry” setting (min 2, max 10) for built-in transform providers, available in both add and edit provider modals.
    • Exposed/returned the setting in provider details and provider listing payloads (including API responses), with localized UI label and hint.
  • Bug Fixes

    • Improved OpenAI bridge resilience for transient upstream failures using bounded retries with backoff across non-streaming and streaming requests.
    • Updated the Prompts panel sub-tab switching behavior to reset scroll when reselecting the active tab.
  • Tests

    • Added unit coverage to verify retry behavior honors the configured maxRetries limit.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7bd2ce77-c0cd-40ef-ad60-5d76cef9c161

📥 Commits

Reviewing files that changed from the base of the PR and between 74af750 and fb8b9b4.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • package.json
  • web-ui/modules/app.methods.agents.mjs
✅ Files skipped from review due to trivial changes (1)
  • package.json
📜 Recent review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: CodeRabbit
🔇 Additional comments (1)
web-ui/modules/app.methods.agents.mjs (1)

711-724: LGTM!


📝 Walkthrough

Walkthrough

Adds configurable, bounded OpenAI-bridge retries across provider configuration, persistence, API responses, runtime forwarding, and the web UI. Bridge conversion and SSE handling are extracted into a shared runtime module, with tests covering retry behavior and provider state.

Changes

OpenAI bridge retry and runtime

Layer / File(s) Summary
Transient retry utility
cli/openai-bridge-retry.js
Adds bounded retry normalization, transient network error detection, exponential backoff, and retry execution.
Bridge conversion and streaming runtime
cli/openai-bridge-runtime.js, cli/openai-bridge.js
Centralizes Responses/Chat conversion, tool handling, SSE translation, fallback detection, JSON proxying, and runtime wiring.
Provider persistence and request wiring
cli.js, cli/openai-bridge.js
Persists maxRetries, updates TOML values, exposes retry settings through provider APIs, and forwards them to upstream operations.
Provider UI, validation, and tests
web-ui/..., tests/unit/..., package.json
Adds retry fields, validation, localization, modal behavior, payload handling, version updates, and retry/provider-state tests.

Prompts tab presentation

Layer / File(s) Summary
Prompts markdown tabs
web-ui/partials/index/panel-prompts.html, web-ui/res/web-ui-render.precompiled.js, web-ui/styles/modals-core.css, web-ui/modules/app.methods.agents.mjs
Replaces the prompts segmented control with accessible tablist markup, dedicated active-tab styling, and same-tab scroll reset behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant OpenAI Bridge
  participant Retry Utility
  participant Upstream API
  Client->>OpenAI Bridge: Send bridge request
  OpenAI Bridge->>Retry Utility: Execute with maxRetries
  Retry Utility->>Upstream API: Forward request
  Upstream API-->>Retry Utility: Response or transient failure
  Retry Utility-->>OpenAI Bridge: Final upstream result
  OpenAI Bridge-->>Client: Responses-compatible response
Loading

Possibly related PRs

Suggested reviewers: ymkiux

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: splitting OpenAI bridge runtime and retry logic into separate layers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/codex-bridge-retry-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cli.js (1)

12870-12886: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use the already-resolved upstream.maxRetries instead of re-deriving from config.toml.

upstream (from resolveOpenaiBridgeUpstream) already carries maxRetries sourced from OPENAI_BRIDGE_SETTINGS_FILE — the value actually used by retryTransientRequest at request time. Instead, this handler discards it and re-reads config.toml's codexmate_bridge_max_retries mirror via resolveProviderOpenaiBridgeMaxRetries(provider). If the two stores ever drift (partial write failure, manual edit, pre-existing provider without the TOML field), the UI's edit form will be pre-filled with a stale value, and saving without touching this field will silently overwrite the real setting in OPENAI_BRIDGE_SETTINGS_FILE with the stale one. The same pattern recurs in buildMcpProviderListPayload (Lines 15116-15131), which also ignores upstream.maxRetries for its openaiBridgeMaxRetries field.

🐛 Suggested fix
-                            // 不返回 apiKey(敏感信息),仅返回用户填过的上游 URL
-                            const config = readConfig();
-                            const provider = config.model_providers && config.model_providers[name];
-                            const providerMaxRetries = resolveProviderOpenaiBridgeMaxRetries(provider);
-                            result = { baseUrl: upstream.baseUrl, hasApiKey: !!(upstream.apiKey), maxRetries: providerMaxRetries };
+                            // 不返回 apiKey(敏感信息),仅返回用户填过的上游 URL
+                            result = { baseUrl: upstream.baseUrl, hasApiKey: !!(upstream.apiKey), maxRetries: upstream.maxRetries };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli.js` around lines 12870 - 12886, Use the already-resolved
upstream.maxRetries when constructing the openai-bridge-get-provider result
instead of re-reading config.toml through resolveProviderOpenaiBridgeMaxRetries.
Also update buildMcpProviderListPayload to populate openaiBridgeMaxRetries from
its resolved upstream.maxRetries, preserving the request-time settings as the
source of truth.
🧹 Nitpick comments (2)
cli/openai-bridge-runtime.js (1)

1339-1797: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Optional: deduplicate the HTTP request scaffolding.

streamChatCompletionsAsResponsesSse, streamResponsesSse, and proxyRequestJson repeat near-identical setup (URL parse, transport selection, header/Content-Length assembly, maxBytes/timeoutMs normalization, finish/settled guard, abortUpstream on res close). Extracting a small helper for request construction and the settle/abort plumbing would reduce drift risk across the three paths (e.g., the timeout asymmetry noted above).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/openai-bridge-runtime.js` around lines 1339 - 1797, The HTTP request
setup and settlement/abort plumbing is duplicated across
streamChatCompletionsAsResponsesSse, streamResponsesSse, and proxyRequestJson.
Extract a focused shared helper for URL/transport selection, headers and
Content-Length, maxBytes/timeoutMs normalization, settled resolution, and
response-close abortion, then update all three functions to use it while
preserving their endpoint-specific streaming and error behavior.
cli/openai-bridge.js (1)

167-200: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

maxRetries has no existing-value fallback, unlike headers.

headers falls back to existingHeaders when no new headers are supplied, but maxRetries is always taken from normalizeBridgeMaxRetries(options && options.maxRetries) with no fallback to the currently stored value. Any future caller that omits options.maxRetries will silently reset a previously configured retry count back to the default. Today's callers in cli.js work around this by always resolving a value up front (including one path that re-derives it from config.toml via resolveProviderOpenaiBridgeMaxRetries), but that's fragile — it would be safer to make this function preserve the existing value itself.

♻️ Suggested fix
     const existing = settings && settings.providers ? settings.providers[name] : null;
     const existingHeaders = existing && typeof existing === 'object' && !Array.isArray(existing)
         ? normalizeHeadersMap(existing.headers || existing.extraHeaders || existing.extra_headers || null)
         : {};
+    const existingMaxRetries = existing && typeof existing === 'object' && !Array.isArray(existing) && existing.maxRetries !== undefined
+        ? existing.maxRetries
+        : undefined;
+    const maxRetries = normalizeBridgeMaxRetries(
+        options && options.maxRetries !== undefined ? options.maxRetries : existingMaxRetries
+    );
     const next = {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/openai-bridge.js` around lines 167 - 200, Update
upsertOpenaiBridgeProvider to preserve the existing provider’s maxRetries when
options.maxRetries is omitted, mirroring the existingHeaders fallback. Read and
normalize the stored value from existing before selecting the new value, while
continuing to use the normalized option when explicitly supplied.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/openai-bridge-runtime.js`:
- Around line 1538-1541: The request timeout in
streamChatCompletionsAsResponsesSse remains active after the upstream response
switches to SSE. Clear or otherwise disable that timeout at the point SSE
acceptance is confirmed, matching streamResponsesSse, so idle streaming cannot
invoke finish with a timeout after partial output or emit duplicate terminal
responses.

In `@web-ui/modules/app.methods.providers.mjs`:
- Around line 89-97: Update the provider validation flow around
normalizeBridgeMaxRetries and the openaiBridgeMaxRetries check so validation
uses the raw, unclamped draft input first and sets errors.openaiBridgeMaxRetries
when the value is below 2; only normalize the value after validation for
submission. Preserve submit-time clamping and add a regression test covering the
inline minimum-retries error message.

In `@web-ui/modules/i18n/locales/zh-tw.mjs`:
- Around line 491-492: Update the hint.transformMaxRetries translation to
mention the setting’s maximum value of 10 alongside the existing default and
minimum values, while preserving the surrounding retry-count explanation.

In `@web-ui/modules/i18n/locales/zh.mjs`:
- Around line 491-492: Update the `hint.transformMaxRetries` translation in the
Chinese locale to mention the enforced maximum of 10 alongside the existing
default of 2 and minimum of 2; leave the `field.transformMaxRetries` label
unchanged.

---

Outside diff comments:
In `@cli.js`:
- Around line 12870-12886: Use the already-resolved upstream.maxRetries when
constructing the openai-bridge-get-provider result instead of re-reading
config.toml through resolveProviderOpenaiBridgeMaxRetries. Also update
buildMcpProviderListPayload to populate openaiBridgeMaxRetries from its resolved
upstream.maxRetries, preserving the request-time settings as the source of
truth.

---

Nitpick comments:
In `@cli/openai-bridge-runtime.js`:
- Around line 1339-1797: The HTTP request setup and settlement/abort plumbing is
duplicated across streamChatCompletionsAsResponsesSse, streamResponsesSse, and
proxyRequestJson. Extract a focused shared helper for URL/transport selection,
headers and Content-Length, maxBytes/timeoutMs normalization, settled
resolution, and response-close abortion, then update all three functions to use
it while preserving their endpoint-specific streaming and error behavior.

In `@cli/openai-bridge.js`:
- Around line 167-200: Update upsertOpenaiBridgeProvider to preserve the
existing provider’s maxRetries when options.maxRetries is omitted, mirroring the
existingHeaders fallback. Read and normalize the stored value from existing
before selecting the new value, while continuing to use the normalized option
when explicitly supplied.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0e26c54c-4d1a-4ef9-a74c-ac3bd4276b1f

📥 Commits

Reviewing files that changed from the base of the PR and between 042f455 and fbe6e3c.

📒 Files selected for processing (18)
  • cli.js
  • cli/openai-bridge-retry.js
  • cli/openai-bridge-runtime.js
  • cli/openai-bridge.js
  • tests/unit/openai-bridge-upstream-responses.test.mjs
  • tests/unit/provider-default-names.test.mjs
  • tests/unit/provider-share-command.test.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/providers-validation.test.mjs
  • web-ui/app.js
  • web-ui/modules/app.methods.providers.mjs
  • web-ui/modules/i18n/locales/en.mjs
  • web-ui/modules/i18n/locales/ja.mjs
  • web-ui/modules/i18n/locales/vi.mjs
  • web-ui/modules/i18n/locales/zh-tw.mjs
  • web-ui/modules/i18n/locales/zh.mjs
  • web-ui/partials/index/modals-basic.html
  • web-ui/res/web-ui-render.precompiled.js
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: CodeRabbit
🧰 Additional context used
🪛 ast-grep (0.44.1)
cli/openai-bridge-retry.js

[warning] 37-37: Avoid using the initial state variable in setState
Context: setTimeout(r, delay)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

cli.js

[warning] 10040-10040: Detects non-literal values in regular expressions
Context: new RegExp(^(\\s*${escapedFieldName}\\s*=\\s*)([-+]?\\d+(?:\\.\\d+)?)(\\s+#.*)?$, 'mg')
Note: [CWE-1333] Inefficient Regular Expression Complexity (ReDoS via non-literal RegExp).

(detect-non-literal-regexp)

cli/openai-bridge-runtime.js

[warning] 1647-1649: Avoid using the initial state variable in setState
Context: setTimeout(() => {
failAcceptedStream('upstream stream idle timeout');
}, timeoutMs)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🔇 Additional comments (19)
cli/openai-bridge-retry.js (1)

1-62: LGTM!

cli.js (3)

2565-2584: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate clamp/default logic vs. openai-bridge-runtime.js's normalizeBridgeMaxRetries.

This mirrors the concern raised in cli/openai-bridge.js: two independent implementations of essentially the same "clamp retries, default 2" logic exist in different files/files, governing two different persisted copies of the same setting (config.toml vs OPENAI_BRIDGE_SETTINGS_FILE). Consider sharing a single normalizer to guarantee they never diverge.


9873-9877: LGTM! cmdUpdate's openaiBridgeMaxRetries option handling and the new replaceTomlNumberField helper (mirroring the existing string/boolean field replacers) are wired consistently, and the two writes (settings.json + config.toml) happen together whenever the field is explicitly changed. The static-analysis "non-literal RegExp" hint on replaceTomlNumberField is a false positive here — fieldName is always a hardcoded literal ('codexmate_bridge_max_retries'), not user input.

Also applies to: 10037-10062, 10097-10150


2586-2708: LGTM! addProviderToConfig/updateProviderInConfig consistently thread openaiBridgeMaxRetries into both upsertOpenaiBridgeProvider and the codexmate_bridge_max_retries TOML line.

Also applies to: 2710-2738

cli/openai-bridge.js (2)

378-582: LGTM! All five retryTransientRequest call sites consistently forward { maxRetries: upstream.maxRetries }.


89-108: 📐 Maintainability & Code Quality

No mismatch in max-retry normalizers normalizeBridgeMaxRetries uses the same default (2) and clamp ([2, 10]) as normalizeOpenaiBridgeMaxRetries, so the UI/config and runtime paths stay aligned.

			> Likely an incorrect or invalid review comment.
web-ui/app.js (1)

287-289: LGTM! Default openaiBridgeMaxRetries: 2 matches the backend's normalizeOpenaiBridgeMaxRetries default.

tests/unit/openai-bridge-upstream-responses.test.mjs (1)

96-141: LGTM! Solid end-to-end coverage of provider-configured maxRetries against a transient upstream failure, with proper teardown.

tests/unit/provider-share-command.test.mjs (1)

1470-1472: LGTM! Test double updated to match buildMcpProviderListPayload's new dependency on resolveProviderOpenaiBridgeMaxRetries.

tests/unit/provider-switch-regression.test.mjs (1)

273-273: LGTM! Matches the updated default editingProvider shape in web-ui/app.js.

web-ui/modules/app.methods.providers.mjs (2)

44-49: LGTM!


151-243: LGTM!

Also applies to: 337-365, 367-439, 441-501, 580-584

web-ui/partials/index/modals-basic.html (1)

56-68: LGTM!

Also applies to: 107-119

web-ui/res/web-ui-render.precompiled.js (1)

7098-7128: LGTM!

Also applies to: 7241-7271

web-ui/modules/i18n/locales/en.mjs (1)

491-492: LGTM!

web-ui/modules/i18n/locales/ja.mjs (1)

493-494: LGTM!

web-ui/modules/i18n/locales/vi.mjs (1)

711-712: LGTM!

tests/unit/provider-default-names.test.mjs (1)

41-42: LGTM!

tests/unit/providers-validation.test.mjs (1)

25-26: LGTM!

Also applies to: 97-97, 183-213

Comment on lines +1538 to +1541
upstreamReq.setTimeout(timeoutMs, () => {
try { upstreamReq.destroy(new Error('timeout')); } catch (_) {}
finish({ ok: false, error: 'timeout' });
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how the streamed result (ok/status) is consumed after streamChatCompletionsAsResponsesSse
rg -nP -C8 'streamChatCompletionsAsResponsesSse' cli/openai-bridge.js

Repository: SakuraByteCore/codexmate

Length of output: 2415


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- cli/openai-bridge-runtime.js around streamChatCompletionsAsResponsesSse ---'
nl -ba cli/openai-bridge-runtime.js | sed -n '1460,1605p'

echo
echo '--- cli/openai-bridge-runtime.js around streamResponsesSse ---'
nl -ba cli/openai-bridge-runtime.js | sed -n '1605,1735p'

echo
echo '--- cli/openai-bridge.js around the stream call site ---'
nl -ba cli/openai-bridge.js | sed -n '450,530p'

Repository: SakuraByteCore/codexmate

Length of output: 282


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- cli/openai-bridge-runtime.js around streamChatCompletionsAsResponsesSse ---'
cat -n cli/openai-bridge-runtime.js | sed -n '1460,1605p'

echo
echo '--- cli/openai-bridge-runtime.js around streamResponsesSse ---'
cat -n cli/openai-bridge-runtime.js | sed -n '1605,1735p'

echo
echo '--- cli/openai-bridge.js around the stream call site ---'
cat -n cli/openai-bridge.js | sed -n '450,530p'

Repository: SakuraByteCore/codexmate

Length of output: 20131


🏁 Script executed:

node - <<'JS'
const http = require('http');

async function test(clearAfterStart) {
  return new Promise((resolve, reject) => {
    const server = http.createServer((req, res) => {
      res.writeHead(200, { 'Content-Type': 'text/event-stream' });
      res.write('data: first\n\n');
      if (clearAfterStart) {
        // Simulate the accepted-stream behavior from streamResponsesSse.
        req.setTimeout(0);
      }
      setTimeout(() => {
        try { res.write('data: second\n\n'); } catch {}
        try { res.end(); } catch {}
      }, 250);
    });

    server.listen(0, '127.0.0.1', () => {
      const { port } = server.address();
      const req = http.request({
        host: '127.0.0.1',
        port,
        path: '/',
        method: 'GET'
      }, (res) => {
        res.on('data', () => {});
        res.on('end', () => {
          server.close(() => resolve({ clearAfterStart, timedOut }));
        });
      });

      let timedOut = false;
      req.setTimeout(100, () => {
        timedOut = true;
        try { req.destroy(new Error('timeout')); } catch {}
      });
      req.on('error', () => {});
      req.end();

      setTimeout(() => {
        if (!server.listening) return;
        server.close(() => resolve({ clearAfterStart, timedOut }));
      }, 500);
    });
  });
}

(async () => {
  console.log(await test(false));
  console.log(await test(true));
})();
JS

Repository: SakuraByteCore/codexmate

Length of output: 249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- timeout-related references in cli/openai-bridge-runtime.js ---'
rg -n "setTimeout\\(|streamIdleTimer|writeSse\\(|response\\.failed|finish\\(\\{ ok: false" cli/openai-bridge-runtime.js

echo
echo '--- streamChatCompletionsAsResponsesSse full function context ---'
python3 - <<'PY'
from pathlib import Path
text = Path('cli/openai-bridge-runtime.js').read_text()
start = text.index('function streamChatCompletionsAsResponsesSse')
end = text.index('function streamResponsesSse')
for i, line in enumerate(text[start:end].splitlines(), 1):
    print(f"{i:5d} {line}")
PY

echo
echo '--- retryTransientRequest implementation ---'
python3 - <<'PY'
from pathlib import Path
text = Path('cli/openai-bridge-runtime.js').read_text()
start = text.index('async function retryTransientRequest')
end = text.index('function isTransientNetworkError')
for i, line in enumerate(text[start:end].splitlines(), 1):
    print(f"{i:5d} {line}")
PY

Repository: SakuraByteCore/codexmate

Length of output: 15114


🌐 Web query:

Node.js ClientRequest.setTimeout response stream inactivity timeout mid-stream docs

💡 Result:

In Node.js, the ClientRequest.setTimeout() method behaves as an inactivity timer, not a total request duration timer [1][2]. It monitors the underlying socket for inactivity—meaning it triggers only if no data has been received or sent over the socket for the specified duration [1][3]. Key behaviors regarding mid-stream inactivity: - Inactivity Reset: The timeout timer is reset whenever new data is received [1][4]. If you are streaming a large response, the timer will not fire as long as data packets continue to arrive within the timeout interval, even if the total time taken to stream the response far exceeds the timeout value [2]. - Socket Dependency: The timer is only armed once a socket has been assigned to the request and successfully connected [1][5]. It does not account for the time spent in the DNS lookup or the initial TCP connection handshake [5][6]. - Manual Destruction: When the timeout event is emitted, Node.js notifies you that the socket has been idle, but it does not automatically abort or destroy the request [7][3]. You must explicitly call request.destroy() or request.abort() within the timeout handler to stop the operation [7][3]. To enforce a "total duration" timeout (i.e., a maximum time from request initiation to completion), you must implement a separate timer (using setTimeout) that runs independently of the socket's inactivity state [5]. This manual timer should be cleared when the request finishes or errors out [5][6].

Citations:


Clear the request timeout once SSE is accepted. In streamChatCompletionsAsResponsesSse, the request-level timeout stays armed for the whole stream, unlike streamResponsesSse which clears it after switching to SSE. An idle upstream stream can then resolve { ok: false, error: 'timeout' } after partial output has already been written, and cli/openai-bridge.js will emit a second response.failed/done on the same res.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/openai-bridge-runtime.js` around lines 1538 - 1541, The request timeout
in streamChatCompletionsAsResponsesSse remains active after the upstream
response switches to SSE. Clear or otherwise disable that timeout at the point
SSE acceptance is confirmed, matching streamResponsesSse, so idle streaming
cannot invoke finish with a timeout after partial output or emit duplicate
terminal responses.

Comment on lines +89 to 97
const useTransform = !!(draft && draft.useTransform);
const openaiBridgeMaxRetries = normalizeBridgeMaxRetries(draft && draft.openaiBridgeMaxRetries);
const errors = {
name: '',
url: '',
key: '',
model: ''
model: '',
openaiBridgeMaxRetries: ''
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Minimum-retries validation is unreachable dead code.

openaiBridgeMaxRetries is normalized via normalizeBridgeMaxRetries (Line 90) before the < 2 check (Line 127) runs, and that helper always clamps its output to at least 2. So openaiBridgeMaxRetries < 2 can never be true, and errors.openaiBridgeMaxRetries can never be set — the “重试次数最小为 2” message will never show, regardless of what the user types. This silently defeats the minimum-retries validation that the PR objectives explicitly call out ("Web UI screenshots for ... minimum validation").

The fix is to validate the raw (un-clamped) input before normalizing:

🐛 Proposed fix
     const useTransform = !!(draft && draft.useTransform);
-    const openaiBridgeMaxRetries = normalizeBridgeMaxRetries(draft && draft.openaiBridgeMaxRetries);
+    const rawMaxRetries = Number(draft && draft.openaiBridgeMaxRetries);
+    const openaiBridgeMaxRetries = normalizeBridgeMaxRetries(draft && draft.openaiBridgeMaxRetries);
     const errors = {
         name: '',
         url: '',
         key: '',
         model: '',
         openaiBridgeMaxRetries: ''
     };
@@
-    if (useTransform && openaiBridgeMaxRetries < 2) {
+    if (useTransform && Number.isFinite(rawMaxRetries) && rawMaxRetries < 2) {
         errors.openaiBridgeMaxRetries = '重试次数最小为 2';
     }

Also worth adding a regression test for this once fixed (the new test in providers-validation.test.mjs only covers submit-time clamping, not the inline error message).

Also applies to: 127-141

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.methods.providers.mjs` around lines 89 - 97, Update the
provider validation flow around normalizeBridgeMaxRetries and the
openaiBridgeMaxRetries check so validation uses the raw, unclamped draft input
first and sets errors.openaiBridgeMaxRetries when the value is below 2; only
normalize the value after validation for submission. Preserve submit-time
clamping and add a regression test covering the inline minimum-retries error
message.

Comment on lines +491 to +492
'field.transformMaxRetries': '內建轉換重試次數',
'hint.transformMaxRetries': '預設 2,最小 2;表示失敗後最多重試的次數。',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Hint text omits the max-10 boundary.

The PR adds a max UI boundary of 10 for this setting, but the hint only mentions the default/min ("預設 2,最小 2"). Consider mentioning the upper bound too, so users aren't confused when a higher value gets silently clamped.

✏️ Suggested wording
-    'hint.transformMaxRetries': '預設 2,最小 2;表示失敗後最多重試的次數。',
+    'hint.transformMaxRetries': '預設 2,範圍 2-10;表示失敗後最多重試的次數。',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'field.transformMaxRetries': '內建轉換重試次數',
'hint.transformMaxRetries': '預設 2,最小 2;表示失敗後最多重試的次數。',
'field.transformMaxRetries': '內建轉換重試次數',
'hint.transformMaxRetries': '預設 2,範圍 2-10;表示失敗後最多重試的次數。',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/i18n/locales/zh-tw.mjs` around lines 491 - 492, Update the
hint.transformMaxRetries translation to mention the setting’s maximum value of
10 alongside the existing default and minimum values, while preserving the
surrounding retry-count explanation.

Comment on lines +491 to +492
'field.transformMaxRetries': '内建转换重试次数',
'hint.transformMaxRetries': '默认 2,最小 2;表示失败后最多重试的次数。',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Hint text omits the max-10 boundary.

Same issue as zh-tw.mjs: the hint only states "默认 2,最小 2" without mentioning the upper bound of 10 that the UI enforces.

✏️ Suggested wording
-    'hint.transformMaxRetries': '默认 2,最小 2;表示失败后最多重试的次数。',
+    'hint.transformMaxRetries': '默认 2,范围 2-10;表示失败后最多重试的次数。',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/i18n/locales/zh.mjs` around lines 491 - 492, Update the
`hint.transformMaxRetries` translation in the Chinese locale to mention the
enforced maximum of 10 alongside the existing default of 2 and minimum of 2;
leave the `field.transformMaxRetries` label unchanged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
web-ui/partials/index/panel-prompts.html (1)

8-10: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider adding tabindex management for the ARIA tab pattern.

The role="tablist" / role="tab" / aria-selected semantics are correctly applied. However, the WAI-ARIA tab pattern recommends roving tabindex — the active tab should have tabindex="0" and inactive tabs tabindex="-1" — so that keyboard users tab into the tab list once and use arrow keys to switch. Currently both buttons are natively focusable and in the default tab order, which works but deviates from the standard pattern.

If manual-activation-with-all-tabs-focusable is intentional, this can be skipped. Otherwise, consider adding :tabindex="promptsSubTab === 'codex' ? 0 : -1" (and the inverse for the other button) plus arrow-key handling in switchPromptsSubTab.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/panel-prompts.html` around lines 8 - 10, Implement
roving tabindex for the prompts tablist: update both tab buttons in the prompts
sub-tab markup so the active tab has tabindex 0 and the inactive tab has
tabindex -1, based on promptsSubTab. Extend switchPromptsSubTab to support
keyboard arrow navigation while preserving the existing click behavior and
active-tab selection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@web-ui/partials/index/panel-prompts.html`:
- Around line 8-10: Implement roving tabindex for the prompts tablist: update
both tab buttons in the prompts sub-tab markup so the active tab has tabindex 0
and the inactive tab has tabindex -1, based on promptsSubTab. Extend
switchPromptsSubTab to support keyboard arrow navigation while preserving the
existing click behavior and active-tab selection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f7002537-7e96-4671-aa7d-b35c2456ef56

📥 Commits

Reviewing files that changed from the base of the PR and between fbe6e3c and 74af750.

📒 Files selected for processing (3)
  • web-ui/partials/index/panel-prompts.html
  • web-ui/res/web-ui-render.precompiled.js
  • web-ui/styles/modals-core.css
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: CodeRabbit
🔇 Additional comments (2)
web-ui/res/web-ui-render.precompiled.js (1)

6659-6678: LGTM!

web-ui/styles/modals-core.css (1)

696-727: LGTM!

@ymkiux
ymkiux merged commit e65caf5 into main Jul 11, 2026
12 checks passed
@ymkiux
ymkiux deleted the feat/codex-bridge-retry-config branch July 11, 2026 09:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants